﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO.Ports;
using System.Threading;
using System.Collections;
using OpenNETCF.IO.Serial;
using System.Windows.Forms;

namespace QuadRemote.Core
{
    class Bluetooth
    {
        private Port port;

        public delegate void commandParserSpec(CommandIn args);
        public commandParserSpec[] commandParsers;

        private const long RESEND_AFTER_TICKS = TimeSpan.TicksPerMillisecond * 5000;
        private byte[] readBuffer = new byte[0];
        
        public long bytesSent = 0;
        public long bytesSentPayload = 0;
        public long packetsSent = 0;
        public long bytesResent = 0;

        public long bytesReceived = 0;
        public long bytesReceivedPayload = 0;
        public long packetsReceived = 0;
        public long bytesReceivedSkipped = 0;


        public const int STATE_DISCONNECTED = 0;
        public const int STATE_CONNECTING = 1;
        public const int STATE_CONNECTING_FAILED = 2;
        public const int STATE_CONNECTED = 3;
        public const int STATE_CONNECTED_STABLE = 4;
        public const int STATE_CONTROL_TIMEOUT = 5;

        public int state;


        private long timeLastReceived;
        private long timeLastSent;

        private bool threadActive = true;
        private bool portActive = false;
        private bool linkActive = false;

        private List<CommandOut> outgoingPackets;
        private List<CommandOut> outgoingPacketUnconfirmed;
        private byte packetId;
        private byte lastReceivedPacketId;
        
        private static Bluetooth _instance = null;
        public static Bluetooth instance()
        {
            if (_instance == null)
            {
                _instance = new Bluetooth();
            }
            return _instance;
        }

        MainWindow _parent;
        
        public void setParent(MainWindow parent)
        {
            _parent = parent;
        }

        protected Bluetooth()
        {
            outgoingPackets = new List<CommandOut>();
            outgoingPacketUnconfirmed = new List<CommandOut>();

            commandParsers = new commandParserSpec[256];

            state = STATE_DISCONNECTED;
            portActive = false;
        }

        public void closePort()
        {
            Console.WriteLine("BT closePort called");
            if (port == null || !portActive) return;
            lock (port)
            {
                port.Close();
                port.Dispose();
                port = null;
                portActive = false;
                state = STATE_DISCONNECTED;
            }
        }

        public void setPort()
        {
            closePort();

            String portName = Properties.Settings.Default.BTPort;

            if (portName == "") return;

            try
            {
                state = STATE_CONNECTING;
                Console.WriteLine("Bluetooth: connecting to " + portName);
                //SerialPortFixer.Execute(portName);
                //port = new Port(portName, 1, Parity.None, 8, StopBits.One);
                DetailedPortSettings portSettings = new DetailedPortSettings();
                
                portSettings.BasicSettings.BaudRate = BaudRates.CBR_110;
                portSettings.BasicSettings.Parity = OpenNETCF.IO.Serial.Parity.none;
                portSettings.BasicSettings.StopBits = OpenNETCF.IO.Serial.StopBits.one;
                portSettings.BasicSettings.ByteSize = 8;
                portSettings.AbortOnError = true;
                portSettings.OutCTS = false;
                portSettings.RTSControl = RTSControlFlows.handshake;
                portSettings.InX = false;
                portSettings.OutX = false;
                portSettings.TxContinueOnXOff = false;
                port = new Port(portName, portSettings, 16 * 1024, 16 * 1024);
                port.Open();
                
                 while (!port.IsOpen)
                 {
                    Thread.Sleep(1);
                 }
                 port.SThreshold = 1;
                 port.RTSEnable = true;
                 port.OnError += port_OnError;
                 port.PowerEvent += port_PowerEvent;
                 port.TxDone += port_TxDone;
                 //port.DataReceived += port_DataReceived;
                 port.CTSChange += port_CTSChange;
                 port.DSRChange += port_DSRChange;
                 //port.FlagCharReceived += port_FlagCharReceived;
                 port.HighWater += port_HighWater;
                 port.RingChange += port_RingChange;
                 port.RLSDChange += port_RLSDChange;
                 //port.DataReceived += new SerialDataReceivedEventHandler(dataIn);
                 portActive = true;
                 //port.PinChanged += new SerialPinChangedEventHandler(pinChange);
            }
            catch (Exception e)
            {
                portActive = false;
                Console.WriteLine("Bluetooth: could not open port (" + e.Message + ")");
                MessageBox.Show(e.Message);
                
                state = STATE_CONNECTING_FAILED;

                if (port != null)
                {
                    port.Close();
                    port.Dispose();
                    port = null;
                }
            }

            if (port == null) return;
            timeLastReceived = DateTime.Now.Ticks;
            timeLastSent = DateTime.Now.Ticks;
            //Console.WriteLine("Bluetooth: Connected");

            state = STATE_CONNECTED;

            if (!FlightSession.current().Config.isLoaded())
                new CommandOut(CommandId.CMD_CONFIG_SEND_ALL).send();
            if (!FlightSession.current().Log.isLoaded())
               new CommandOut(CommandId.CMD_LOG_SEND_DEFINITIONS).send();
        }

        void port_RLSDChange(bool NewState)
        {
            Console.WriteLine("BT RLSD is now " + (NewState ? 1 : 0));

        }

        void port_RingChange(bool NewState)
        {
            Console.WriteLine("BT RING is now " + (NewState ? 1 : 0));
        }

        void port_HighWater()
        {
            Console.WriteLine("BT Highwater received");
        }

        void port_FlagCharReceived()
        {
            Console.WriteLine("BT FlagChar received");
        }

        void port_DSRChange(bool NewState)
        {
            Console.WriteLine("BT DSR is now " + (NewState ? 1 : 0));
        }

        void port_CTSChange(bool NewState)
        {
            Console.WriteLine("BT CTS is now " + (NewState ? 1 : 0));
        }

        void port_DataReceived()
        {
            Console.WriteLine("BT Data received");
        }

        void port_TxDone()
        {
            Console.WriteLine("BT TX DONE");
        }

        void port_PowerEvent()
        {
            Console.WriteLine("BT PowerEvent");
        }

        void port_OnError(string Description)
        {
            Console.WriteLine("BT Error: " + Description);
        }


    /*    private void dataIn(object sender, SerialDataReceivedEventArgs args)
        {
            Console.Write("DATAIN:");
            byte[] tempBuffer;
            int bytesRead = 0;
            long ticks = DateTime.Now.Ticks;
            timeLastReceived = ticks;

            lock (port)
            {
                try
                {
                    bytesRead = port.BytesToRead;
                }
                catch (Exception e)
                {
                    Console.WriteLine("Bluetooth: dataIn failed to read " + e);
                    return;
                }

                if (bytesRead == 0) return;

                tempBuffer = new byte[bytesRead];
                try
                {
                    port.Read(tempBuffer, 0, bytesRead);
                    //Console.WriteLine("IN: "  + Tools.getHexString(tempBuffer));

                    bytesReceived += bytesRead;
                }
                catch (Exception e)
                {
                    Console.WriteLine("Bluetooth: dataIn failed to read 2" + e);
                    return;
                }
            }

            lock (readBuffer)
            {
                for (int i = 0; i < bytesRead; i++)
                {
                    readBuffer[readBufferPos++] = tempBuffer[i];
                    if (readBufferPos >= READBUFFERSIZE) readBufferPos = 0;
                }
                readBufferLength += bytesRead;
            }
            Console.WriteLine("END");
        }*/

        private void pinChange(object sender, SerialPinChangedEventArgs e)
        {
            //SerialPort sp = (SerialPort)sender;
            Console.WriteLine("Pin Changed " + e.EventType);
        }

        public void stop()
        {
            threadActive = false;
        }

        private int lastLogPacketId = 0;

        private static byte PACKET_START = (byte)'@';
        private static byte PACKET_END = (byte)'^';
        private static byte PACKET_CONFIRM_REQUEST = (byte)'!';
        private static byte PACKET_CONFIRMED = (byte)'%';

        private void handleInput()
        {
            byte[] receivedBytes = port.Input;
            if (receivedBytes.Length == 0) return;
            else
            {
                //Console.WriteLine(Tools.getHexString(receivedBytes));
                byte[] newReadBuffer = new byte[readBuffer.Length + receivedBytes.Length];
                //Console.WriteLine(Tools.getHexString(receivedBytes));
                Array.Copy(readBuffer, newReadBuffer, readBuffer.Length);
                Array.Copy(receivedBytes, 0, newReadBuffer, readBuffer.Length, receivedBytes.Length);
                readBuffer = newReadBuffer;
                bytesReceived += receivedBytes.Length;

                //Console.WriteLine(Tools.getHexString(readBuffer));
            }

            int pos = 0;
            while (pos < readBuffer.Length)
            {
                byte begin = readBuffer[pos++];

                if (begin != PACKET_START && begin != PACKET_CONFIRM_REQUEST && begin != PACKET_CONFIRMED)
                {
                    bytesReceivedSkipped++;
                    //Console.WriteLine("BT: Not matching begin-of-packet " + (int)begin);
                    // illegal start of packet
                    //Console.WriteLine("Illegal start of packet - " + (int)begin);
                    continue;
                }

                if (begin == PACKET_CONFIRM_REQUEST)
                {
                    //Console.WriteLine("BT: Packet confirm request received");
                    byte[] buffer = new byte[1];
                    buffer[0] = PACKET_CONFIRMED;
                    bytesSent++;
                    port.Output = buffer;
                    //Console.WriteLine("BT: Packet confirmed sent");
                    continue;
                }

                if (begin == PACKET_CONFIRMED)
                {
                    //Console.WriteLine("BT: Packet confirmed received");
                    state = STATE_CONNECTED_STABLE;
                    lock (outgoingPackets)
                    {
                        if (outgoingPackets.Count > 0 && outgoingPackets[0].confirmed == false)
                        {
                            outgoingPackets[0].confirmed = true;

                            if (outgoingPackets[0].hasCallback)
                            {
                                outgoingPacketUnconfirmed.Add(outgoingPackets[0]);
                            }

                            outgoingPackets.RemoveAt(0);
                        }
                    }
                    continue;
                }

                if (readBuffer.Length < pos + 4)
                {
                    pos--;
                    break;
                }

                byte packetId = readBuffer[pos++];
                byte packetMSB = readBuffer[pos++];
                byte packetLSB = readBuffer[pos++];

                ushort packetLength = (ushort)((packetLSB << 8) | (packetMSB));

                if (readBuffer.Length < packetLength + pos + 1)
                {
                    pos -= 4;
                    break;
                }

                //Console.WriteLine("BT: Decode packet #" + (int)packetId + " - len " + packetLength);

                if (readBuffer[pos + packetLength] != PACKET_END)
                {
                    Console.WriteLine("BT: Not matching end-of-packet " + (int)readBuffer[pos + packetLength]);
                    Console.WriteLine(Tools.getHexString(readBuffer));
                    bytesReceivedSkipped++;
                    continue;
                }

                if (packetLength > 0)
                {
                    //Console.WriteLine((long)(DateTime.Now.Ticks / TimeSpan.TicksPerMillisecond) + " - " + (int)packetId + " - " + packetLength);

                    if (packetId != lastReceivedPacketId)
                    {
                        byte[] incomingPacket = new byte[packetLength];
                        Array.Copy(readBuffer, pos, incomingPacket, 0, packetLength);

                        bytesReceivedPayload += packetLength;
                        packetsReceived++;

                        if (commandParsers[incomingPacket[0]] != null)
                        {
                            (commandParsers[incomingPacket[0]])(new CommandIn(incomingPacket));
                        }
                        else
                        {
                            Console.WriteLine("Unparsed incoming " + Tools.getHexString(incomingPacket[0]));
                        }
                        lastReceivedPacketId = packetId;
                    }
                    else
                    {
                        bytesReceivedSkipped += 5 + packetLength;
                        Console.WriteLine("Incoming packet discarded; already received");
                    }
                }
                pos += 1 + packetLength;
            }

            {
                byte[] newReadBuffer = new byte[readBuffer.Length - pos];
                Array.Copy(readBuffer, pos, newReadBuffer, 0, newReadBuffer.Length);
                readBuffer = newReadBuffer;
            }
        }

        public void handleOutput()
        {
            lock (outgoingPackets)
            {
                if (outgoingPackets.Count > 0)
                {
                    CommandOut sending = outgoingPackets[0];

                    if (sending.timeSentAt == 0 || sending.timeSentAt < DateTime.Now.Ticks - RESEND_AFTER_TICKS)
                    {
                        if (sending.timeSentAt == 0)
                        {
                            packetId++;
                            sending.packetId = packetId;
                        }
                        else
                        {
                            bytesResent += 6 + sending.body.Length;
                        }

                        int len = sending.body.Length;

                        byte[] packet = new byte[6 + sending.body.Length];
                        packet[0] = PACKET_START;
                        packet[1] = sending.packetId; // (byte)((p.packetId << 4) | ((len + 2) >> 8));
                        packet[2] = (byte)(len & 0xFF);
                        packet[3] = (byte)(len >> 8);
                        Array.Copy(sending.body, 0, packet, 4, sending.body.Length);
                        packet[4 + sending.body.Length] = PACKET_END;
                        packet[5 + sending.body.Length] = PACKET_CONFIRM_REQUEST;

                        bytesReceivedPayload += len;

                        port.Output = packet;

                        sending.timeSentAt = DateTime.Now.Ticks;
                        bytesSent += 6 + len;
                        packetsSent++;

                        timeLastSent = DateTime.Now.Ticks;
                        Console.WriteLine("Priority packet #" + (int)sending.packetId + "  sent: "); //+ Tools.getHexString(packet));
                        //break;
                    }

                }
            }
            timeoutPackets();
        }

        public void timeoutPackets()
        {
            lock (outgoingPackets)
            {
                int i = outgoingPackets.Count - 1;
                while (i >= 0)
                {
                    outgoingPackets[i].checkTimeout();
                    i--;
                }
            }
            lock (outgoingPacketUnconfirmed)
            {
                int i = outgoingPacketUnconfirmed.Count - 1;
                while (i >= 0)
                {
                    outgoingPacketUnconfirmed[i].checkTimeout();
                    i--;
                }
            }
        }

        bool connect;
        public bool Connect
        {
            get
            {
                return connect;
            }

            set
            {
                connect = value;
            }
        }

        public void run()
        {
            Thread.CurrentThread.Name = "Bluetooth";
            long benchSecond = DateTime.Now.Ticks;
            long benchReceived = 0;
            long benchSent = 0;
            connect = false;
            long currentTick;

            while (threadActive)
            {
                Thread.Sleep(new TimeSpan(100));
                currentTick = DateTime.Now.Ticks;

                if (portActive)
                {
                    byte[] tmp = new byte[50];
                    for (int i = 0; i < tmp.Length; i++)
                        tmp[i] = 0x20;
                    //port.Output = tmp;
                    //bytesSent += tmp.Length;

                        // normal code
                    handleInput();
                    if (linkActive)
                    {
                        //sendPriorityPacket(new BluetoothPacket(true, tmp));
                        //sendPriorityPacket(new BluetoothPacket(true, tmp));
                        //sendPriorityPacket(new BluetoothPacket(true, tmp));
                        //sendPriorityPacket(new BluetoothPacket(true, tmp));
                    }
                    handleOutput();
                    if (timeLastReceived < currentTick - TimeSpan.TicksPerSecond * 60)
                    {
                        //closePort();
                        //state = STATE_CONTROL_TIMEOUT;
                        //Console.WriteLine("Bluetooth disconnected - no controlpacket received for 2 seconds");
                    }

                    if (!connect)
                    {
                        closePort();
                    }
                }
                else
                {
                    timeoutPackets();
                    Thread.Sleep(1500);
                    if (connect) setPort();
                }

                if (benchSecond < DateTime.Now.Ticks - TimeSpan.TicksPerSecond * 10)
                {
                    //Console.WriteLine("Bluetooth speed: " + (bytesReceived - benchReceived)/10 + " -- " + (bytesSent - benchSent)/10);
                    benchReceived = bytesReceived;
                    benchSecond = DateTime.Now.Ticks;
                    benchSent = bytesSent;
                }
            }

            if (portActive)
            {
                port.Close();
                port.Dispose();
                portActive = false;
            }

        }

        internal void queueCommand(CommandOut commandOut)
        {
            lock (outgoingPackets)
            {
                outgoingPackets.Add(commandOut);
            }
        }

        internal void dequeueCommand(CommandOut commandOut)
        {
            lock (outgoingPacketUnconfirmed)
            {
                outgoingPackets.Remove(commandOut);
                outgoingPacketUnconfirmed.Remove(commandOut);
            }
        }
    }
}
